Skip to content

fix(components): exclude test files from the published stylesheet's @source scan - #8453

Merged
os-justin merged 1 commit into
mainfrom
claude/issue-8446-components-css-excludes-tests
Sep 8, 2026
Merged

fix(components): exclude test files from the published stylesheet's @source scan#8453
os-justin merged 1 commit into
mainfrom
claude/issue-8446-components-css-excludes-tests

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #8446

Tailwind v4 scans source text, not an import graph. packages/components/src/index.css
declared @source '../src/**/*.{ts,tsx}' with no exclusion, so all 243 test files under
src/**/__tests__/ were sources for the published dist/index.css. A class-shaped token
written as a test's expected value therefore compiled a real utility into the shipped
bundle — which is how a test can create the production utility it is asserting on.

The single cleanest instance found here is not a fixture but an assertion:

packages/components/src/renderers/basic/__tests__/icon-unresolvable-placeholder.test.tsx:146
    expect(svg?.getAttribute('class')).toContain('text-green-500');

.text-green-500 shipped in dist/index.css because that assertion names it.

Two @source not lines are added, matching the spelling already used by fields,
plugin-grid and plugin-kanban.


The measurement

src/index.css compiled through this package's own pipeline — postcss([tailwindPostcss()]),
i.e. step 2 of packages/components/scripts/build-css.mjs — and every Rule node collected
with its at-rule ancestry. Diffing rule sets, not counting them.

File set on disk for both readings (this is the step whose omission would make the table
dishonest — the compile is sensitive to what is on disk, not to what is committed):

  • Working tree exactly 0b1ac58a66ee8ee0ddc59e0fa73301f8647ffbf6; git status --porcelain
    over packages/components/src empty for the before reading.
  • 453 .ts/.tsx files under packages/components/src, 243 of them test-shaped.
  • After reading: the same tree plus this PR's two @source not lines. The new guard test
    file was added afterwards and verified not to change the sheet (byte-identical rule set),
    so it does not contaminate the table.

⚠️ And the working directory, which turned out to matter more than the file set.
src/index.css opens with a bare @import 'tailwindcss', so Tailwind's automatic source
detection is ON and — as packages/fields/src/index.css already documents — resolves against
the process CWD. Same commit, same bytes, two artifacts:

compiled from rules bytes rules removed by this PR
packages/components (where pnpm build runs — authoritative) 1385 → 1377 159 827 → 159 481 8
repo root 3430 → 3427 438 435 → 438 319 3

A repo-root reading would have under-reported this PR's removal by five of eight classes
they survive there only because other packages' sources also name them. The table below is
the package-CWD one.

What disappears — the rule diff

Removed (8). Added (0).

@layer utilities || .flex-grow
@layer utilities || .flex-nowrap
@layer utilities || .h-\[125px\]
@layer utilities || .isolate
@layer utilities || .paused
@layer utilities || .shrink
@layer utilities || .text-green-500
@layer utilities || .w-\[250px\]

Per-class verdict: does any non-test source name it?

The compile diff defines the answer — a rule that disappears when test files stop being
scanned is by construction sourced only from test files. The greps below say which text
produced it. All eight: no production namer.

class what actually produced it production namer
.text-green-500 icon-unresolvable-placeholder.test.tsx:141,146 — a className fixture and the toContain expected value none
.h-[125px] snapshot-critical.test.tsx:321 — a Skeleton element with className="h-[125px] w-[250px] …" + committed snapshot none
.w-[250px] snapshot-critical.test.tsx:309,321,323 + committed snapshot none
.flex-grow form-field-panes.test.tsx:25prose in a JSDoc comment about the CSS property flex-grow: 50 none
.flex-nowrap page-header-title-width.test.tsx:6 — prose in a JSDoc comment none
.paused dialog-popover-focus-scope.test.tsx:76 — a // code comment none
.isolate __tests__ prose about Vitest's isolate: true none
.shrink combobox-long-label.test.tsx:34,36 — a test title and a comment none

Four of the eight were compiled out of English prose in comments. None was ever authored
as styling.

Does removing them bite anyone?

  • Apps that run their own Tailwind — every example app here does (apps/console,
    examples/console-starter, examples/byo-backend-console all @source the package src
    dirs) — generate utilities from their own markup and are unaffected.
  • Authored runtime metadata was already unsupported. The 2026-06-30 amendment to ADR-0080
    under ADR-0065 (content/docs/guide/react-pages.md) states a utility class in page source
    "produces CSS only if that exact class happens to already appear in objectui's own source",
    and os validate warns page-source-className-tailwind. That incidental "happens to
    appear" is precisely what is being removed.
  • The narrow case that can regress: an app importing only the prebuilt
    @object-ui/components/style.css, running no Tailwind of its own, hand-writing one of the
    eight in its own JSX. .isolate, .shrink, .flex-nowrap, .text-green-500 are plausible
    there. .flex-grow and .shrink also have surviving canonical spellings — .grow and
    .shrink-0 stay in the sheet, and this repo already migrated flex-grow-Ngrow-N
    deliberately.

That residual case is why the changeset is minor rather than patch.

Proving the change can fail

The instrument is the compiled stylesheet, so the ablation restores the unrestricted
@source and recompiles. Run from the committed implementation (336bd7a4), tree clean,
with a trap ... EXIT INT TERM holding absolute paths.

Mutation proven on disk, not by an exit code: @source not lines 2 → 0; git hash-object
50c2d90f vs git rev-parse HEAD:packages/components/src/index.css = 75153ac5.

Both legs fire:

  1. The rules come back — all eight reappear in the recompiled sheet. So does a ninth,
    .mt-\[3\.7331px\]: the guard test's own sentinel. That is the defect reproducing live on
    this PR's own new test file, and it is what proves the probe is not inert.
  2. The guard test goes redvitest exits 1 on
    AssertionError: expected '/*! tailwindcss v4.3.3 …' not to contain '3.7331px'.

Restore proven by state: git diff HEAD empty, git hash-object back to 75153ac5,
@source not lines back to 2 — then the guard test passes again.

No dist/ rebuild is involved: the reading compiles src/index.css fresh each time, so there
is no stale-artifact leg to get wrong here.

The guard test

src/__tests__/index-css-scan-excludes-tests.test.ts compiles the stylesheet and asserts
both directions, because a stylesheet compiled from nothing satisfies "no test-sourced
rules" — an implementation strictly worse than this fix (delete the @source line) would pass
a negative-only test.

  • Negative: a sentinel arbitrary-value token (mt-[3.7331px], absent everywhere else in
    the repo) is written in that test file itself, which lives under __tests__/. The probe
    is self-hosted: remove the exclusions and the sentinel compiles and the test goes red. It
    does not depend on any other test continuing to name a fixture class.
  • Positive: .flex-col (production-sourced) must be present, and the sheet must hold more
    than 800 rules.

It passes base: packageRoot to @tailwindcss/postcss so the reading is CWD-independent —
otherwise Vitest (repo root) and pnpm build (package dir) would measure different artifacts,
per the table above.

Out of scope — reported, not swept in (#8446 fences this to components)

search_issues was rate-limited, so per the dispatch these are reported rather than filed
unsearched. None is fixed here.

  1. The issue says components is the only one of three missing this. There are more.
    Sweep of every @source in the repo: excluding correctly — fields (filename only),
    plugin-grid (not mentioned in the issue), plugin-kanban. Missing exclusions
    entirely — packages/runner/src/index.css, apps/console/src/index.css,
    apps/site/app/global.css, and both examples/*/src/index.css.
  2. packages/runner is a published package that re-leaks these exact classes. Its
    index.css scans ../../packages/components/src/** with no test exclusion, so after
    this PR @object-ui/runner's own published sheet still compiles the eight. Its line 10 also
    reads @source './src/**/*.{ts,tsx}', which from packages/runner/src/index.css resolves
    to packages/runner/src/src/a directory that does not exist.
  3. components lacks source(none), so its published sheet is CWD-dependent (1385 vs 3430
    rules, table above) and non-shipped text is a source. Measured: adding
    source(none) on top of this PR removes 23 further rules, including .flex-shrink-0,
    .bg-blue-500, .md:text-2xl, .hover:bg-blue-700:hover and — from a doc comment —
    .origin-[--radix-…] with a literal Unicode ellipsis. fields calls its own source(none)
    "load-bearing, not tidiness" for exactly this reason. This PR's two lines are a strict
    improvement but only a partial fix of the false-green generator
    ; the complete fix is
    sibling parity on source(none), which is a materially larger bundle diff and its own card.
  4. packages/components/CHANGELOG.md is a Tailwind source for the published bundle.
    Proven by injecting a unique token into it and recompiling. .flex-shrink-0 — a deprecated
    v3 alias this repo deliberately migrated away from — ships today solely because the
    changelog entry announcing its removal names it.

Changeset

.changeset/8446-components-css-excludes-test-sources.md@object-ui/components: minor.

node scripts/check-changeset-presence.mjs:

✅ 1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)

Not empty-frontmatter: this changes published bundle contents. Not patch: it removes
behaviour a narrow consumer could depend on, and this repo labels its own breaking changes
minor. major is forbidden for the fixed group and
scripts/check-changeset-no-major.mjs passes.


🤖 Generated with Claude Code

https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S


Generated by Claude Code

…source scan

Tailwind v4 scans source TEXT, so `src/index.css`'s unrestricted
`@source '../src/**/*.{ts,tsx}'` made all 243 test files under
`src/**/__tests__/` sources for the published `dist/index.css`. A class-shaped
token written as a test's expected value therefore compiled a real utility into
the shipped bundle — a test could create the production utility it asserted on.

Add the two `@source not` lines already used by `fields`, `plugin-grid` and
`plugin-kanban`. Measured through this package's own postcss +
`@tailwindcss/postcss` pipeline from the directory `pnpm build` runs in, eight
rules are removed and none added: .flex-grow .flex-nowrap .h-[125px] .isolate
.paused .shrink .text-green-500 .w-[250px]. Every one was named only by a test
file; `.flex-grow` came from a prose JSDoc sentence about the CSS property.

The guard test is self-hosted: its sentinel token lives in a file under
`__tests__/`, so removing the exclusions makes the sentinel compile and the
test go red. It is paired with a positive assertion because a stylesheet
compiled from nothing would satisfy the negative half alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3472.9 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-BTzCMVdp.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 498.87KB 114.10KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 189.15KB 52.56KB
fields (index.js) 243.15KB 61.40KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.52KB 46.34KB
plugin-dashboard (index.js) 131.48KB 34.45KB
plugin-designer (index.js) 213.21KB 43.63KB
plugin-detail (index.js) 248.68KB 63.94KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.58KB 56.63KB
plugin-kanban (index.js) 55.40KB 15.71KB
plugin-list (index.js) 112.74KB 27.70KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.54KB 20.84KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-justin
os-justin added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit 868e825 Sep 8, 2026
34 checks passed
@os-justin
os-justin deleted the claude/issue-8446-components-css-excludes-tests branch September 8, 2026 00:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants